package ioccontainer;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;


public class BeanFactoryImpl implements BeanFactory {
	
	private List<BeanDefinition> beanDefinitions;
	private Map<String, Object> beans = new HashMap<>();
	private String classPath = "testclasses";
	
	public BeanFactoryImpl(List<BeanDefinition> beanDefinitions) throws ClassNotFoundException, AmbiguityException, InvocationTargetException, 
	IllegalAccessException, InstantiationException, NoSuchMethodException {
		this.beanDefinitions = beanDefinitions;
		try {
			loadBeans();
		} catch(ClassNotFoundException cnfe) {
			cnfe.printStackTrace();
		}
	}
	
	@Override
	public Object getBean(String id) throws ClassNotFoundException, AmbiguityException, InvocationTargetException, 
	IllegalAccessException, InstantiationException, NoSuchMethodException {
		return findBeanById(id);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getBean(String id, Class<T> type) throws ClassNotFoundException, AmbiguityException, InvocationTargetException, 
	IllegalAccessException, InstantiationException, NoSuchMethodException {
		return (T) findBeanById(id);
	}
	
	private Object findBeanById(String id) throws ClassNotFoundException, AmbiguityException, InvocationTargetException, 
	IllegalAccessException, InstantiationException, NoSuchMethodException {
		Object bean = null;
		if(!beans.containsKey(id)) {
			bean = createBean(findBeanDefinitionFromId(id));
			beans.put(id, bean);
		} else {
			bean = beans.get(id);
		}				
		return bean;
	}
	
	private void loadBeans() throws ClassNotFoundException, AmbiguityException, InvocationTargetException, 
	IllegalAccessException, InstantiationException, NoSuchMethodException {
		for(BeanDefinition bd : beanDefinitions) {		
			getBean(bd.getAttributes().get("id"));
		}
	}
	
	private Object createBean(BeanDefinition bd) throws ClassNotFoundException, AmbiguityException, InvocationTargetException, 
	IllegalAccessException, InstantiationException, NoSuchMethodException {
		Class<?> clazz = Class.forName(appendClassPath(bd.getAttributes().get("class")));
		Constructor<?> c = findSuitableConstructor(bd, clazz);			
		if(c != null) {
			List<Map<String, Object>> objects = new ArrayList<>();
			for(Map<String, String> m : bd.getConstructorArgs()) {
				for(Entry<String, String> e : m.entrySet()) {				
					Object curObj = null;
					String curObjType = "";
					if(e.getKey().equals("value")) {
						curObjType = "java.lang.String";
						curObj = new String(e.getValue());
					}
					else if(e.getKey().equals("ref")) {
						BeanDefinition curObjDef = findBeanDefinitionFromId(e.getValue());
						curObjType = appendClassPath(curObjDef.getAttributes().get("class"));
						curObj = createBean(curObjDef);
					}
					Map<String, Object> curObjMap = new HashMap<>();
					curObjMap.put(curObjType, curObj);
					objects.add(curObjMap);
				}					
			}						
			Class<?>[] constrParamTypes = c.getParameterTypes();
			int constrParamCount = constrParamTypes.length;
			Object[] constrArgs = new Object[constrParamCount];
			for(int i = 0; i < constrParamCount; i++) {
				String seekArgType = constrParamTypes[i].getName();					
				seekObjLoop:
				for(Map<String, Object> objMap : objects) {					
							for(Entry<String, Object> objEntry : objMap.entrySet()) {								
								if(objEntry.getKey().equals(seekArgType)) {	
									if(objEntry.getValue() != null) {										
										constrArgs[i] = objEntry.getValue();
										objEntry.setValue(null);
										break seekObjLoop;
									}									
								}
							}
						}
					}													
			Object newBean = c.newInstance(constrArgs);
			for(Map<String, String> m : bd.getProperties()) {				
				String fieldName = m.get("name");
				String setterName = "set" + fieldName.substring(0,1).toUpperCase() + fieldName.substring(1,fieldName.length());				
				if(m.get("value") != null) {
					Method setter = newBean.getClass().getDeclaredMethod(setterName, String.class);	
					setter.invoke(newBean, new Object[]{new String(m.get("value"))});					
				} else {
					Object ref = createBean(findBeanDefinitionFromId(m.get("ref")));
					Method setter = newBean.getClass().getDeclaredMethod(setterName, ref.getClass());		
					setter.invoke(newBean, new Object[]{ref});
				}					
			}
			return newBean;
		} else  {
			throw new ClassNotFoundException();
		}
	}
	
	private BeanDefinition findBeanDefinitionFromId(String id) throws ClassNotFoundException, AmbiguityException {
		int idCount = 0;
		BeanDefinition beanDef = null;
		for(BeanDefinition bd : beanDefinitions) {			
			if(bd.getAttributes().get("id").equals(id)) {
				beanDef = bd;
				idCount++;
			}
		}
		if(idCount == 1)
			return beanDef;
		else if(idCount > 1)
			throw new AmbiguityException();
		else if(idCount == 0)
			throw new ClassNotFoundException();
		return null;
	}
	
	private Constructor<?> findSuitableConstructor(BeanDefinition bd, Class<?> clazz) throws ClassNotFoundException {
		int stringArgsTotal = 0;
		int objArgsTotal = 0;
		Map<String, Integer> objArgs = new HashMap<>();
		for(Map<String, String> m : bd.getConstructorArgs()) {
			for(Entry<String, String> e : m.entrySet()) {
				if(e.getKey().equals("value")) {
					stringArgsTotal++;
				} else if(e.getKey().equals("ref")) {
					String className = null;
					for(BeanDefinition curBd : beanDefinitions) {			
						if(curBd.getAttributes().get("id").equals(e.getValue())) {
							className = appendClassPath(curBd.getAttributes().get("class"));
						}
					}
					if(className != null) {
						objArgsTotal++;
						if(objArgs.containsKey(className)) {
							objArgs.put(className, objArgs.get(className) + 1);
						} else {
							objArgs.put(className, 1);
						}
					} else throw new ClassNotFoundException();
				}
			}						
		}		
		int stringArgsCount = stringArgsTotal;
		Map<String, Integer> objArgsCount = new HashMap<>(objArgs);		
		for(Constructor<?> c : clazz.getConstructors()) {			
			if(c.getParameterTypes().length != stringArgsTotal + objArgsTotal)
				continue;
			for(Class<?> paramClass : c.getParameterTypes()) {					
				if(paramClass.getName().equals("java.lang.String")) {
					stringArgsCount--;
				} else if(objArgsCount.containsKey(paramClass.getName())) {
					objArgsCount.put(paramClass.getName(), objArgsCount.get(paramClass.getName()) - 1);
				}
			}
			if(stringArgsCount == 0) {
				boolean found = true;
				Iterator<Integer> it = objArgsCount.values().iterator();
				while(it.hasNext()) {
					if(it.next() != 0)
						found = false;
				}
				if(found)
					return c;
			}
		}
		return null;
	}
	
	private String appendClassPath(String path) {
		path = classPath + "." + path;
		return path;
	}
	
}